package com.mxx.common.data.jpa;

import com.mxx.common.data.jpa.auditing.SpecInterceptor;
import com.mxx.common.data.jpa.auditing.SpecInterceptorHandler;
import com.mxx.common.data.utils.EntityReflectInvoke;
import com.mxx.common.exception.CommentException;
import com.mxx.common.spring.ApplicationContextHelper;
import com.mxx.common.utils.ClassUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.data.jpa.domain.Specification;
import org.springframework.data.jpa.repository.query.QueryUtils;
import org.springframework.data.jpa.repository.support.JpaEntityInformation;

import javax.persistence.EntityManager;
import javax.persistence.Query;
import javax.persistence.TypedQuery;
import javax.persistence.criteria.*;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import java.util.stream.Collectors;

@Slf4j
public class JpaOperationHandler<T, ID> {

    private SpecInterceptorHandler interceptorHandler;
    private JpaEntityInformation<T, ID> entityInformation;
    private EntityManager entityManager;
    private final Class<T> domainClass;
    private final String idName;
    private boolean objectId;

    public JpaOperationHandler(JpaEntityInformation<T, ID> entityInformation, EntityManager entityManager) {
        this(ApplicationContextHelper.getBean(SpecInterceptorHandler.class), entityInformation, entityManager);
    }

    public JpaOperationHandler(SpecInterceptorHandler interceptorHandler, JpaEntityInformation<T, ID> entityInformation, EntityManager entityManager) {
        this.interceptorHandler = interceptorHandler;
        this.entityInformation = entityInformation;
        this.entityManager = entityManager;
        this.idName = entityInformation.getIdAttribute().getName();
        this.domainClass = entityInformation.getJavaType();
        this.objectId = !ClassUtil.isBaseClass(entityInformation.getIdType());

    }

    public Object baseQuery(Class voClass, Specification<T> spec, Pageable pageable, String... columns) {
        SpecInterceptor.SpecState state = SpecInterceptor.SpecState.select(domainClass, spec);
        interceptorHandler.doSelect(state);

        CriteriaBuilder criteriaBuilder = entityManager.getCriteriaBuilder();
        CriteriaQuery criteriaQuery = criteriaBuilder.createQuery();
        Root root = criteriaQuery.from(domainClass);

        if (columns.length != 0) {
            JpaOperationUtil.selector(columns, root, criteriaQuery);
        } else if (voClass != null) {
            columns = EntityReflectInvoke.getSourceFieldFromTarget(voClass, domainClass);
            JpaOperationUtil.selector(columns, root, criteriaQuery);
        } else {
            criteriaQuery.select(root);
        }
        Predicate predicate = state.getSpec().toPredicate(root, criteriaQuery, criteriaBuilder);

        if (predicate != null) {
            criteriaQuery.where(predicate);
        }

        Sort sort = pageable.getSort();
        if (sort.isSorted()) {
            criteriaQuery.orderBy(QueryUtils.toOrders(sort, root, criteriaBuilder));
        }
        TypedQuery<T> typedQuery = entityManager.createQuery(criteriaQuery);

        Object object = JpaOperationUtil.getResult(entityManager, domainClass, typedQuery, spec, pageable);
        final String[] finalColumns = columns;
        if (columns.length > 1) {
            if (object instanceof List) {
                List<Object[]> objects = (List<Object[]>) object;
                List list = objects.stream().map(p -> JpaOperationUtil.toDomainObject(domainClass, voClass, finalColumns, p)).collect(Collectors.toList());
                return list;
            } else if (object instanceof Page) {
                Page page = (Page) object;
                return page.map(p -> JpaOperationUtil.toDomainObject(domainClass, voClass, finalColumns, (Object[]) p));
            }
        }
        return object;
    }


    public int baseUpdate(UpdateSpecification<T> spec) {
        SpecInterceptor.SpecState state = SpecInterceptor.SpecState.select(domainClass, spec);
        interceptorHandler.doUpdate(state);
        if (state.getCurrentType() == SpecInterceptor.SpecType.DELETE) {
            return baseDelete(state.getSpec());
        }

        CriteriaBuilder criteriaBuilder = entityManager.getCriteriaBuilder();
        CriteriaUpdate<T> criteriaUpdate = criteriaBuilder.createCriteriaUpdate(domainClass);
        CriteriaQuery<T> criteriaQuery = criteriaBuilder.createQuery(domainClass);
        Root<T> root = criteriaUpdate.from(domainClass);

        UpdateSpecification.Result result = state.getUpdateSpec().buildUpdate(root, criteriaBuilder, criteriaQuery, criteriaUpdate);

        Predicate predicate = result.getPredicate();

        if (result.getUpdateFieldNumber() == 0) {
            log.error("updateFieldNumber is zero . break current update");
            return 0;
        }

        if ((!result.isAllowNullCondition() && predicate == null)) {
            throw new CommentException("The query criteria cannot be blank !");
        }

        if (predicate != null) {
            criteriaUpdate.where(predicate);
        }

        Query query = entityManager.createQuery(criteriaUpdate);
        return query.executeUpdate();
    }

    public int baseDelete(Specification<T> spec) {
        SpecInterceptor.SpecState state = SpecInterceptor.SpecState.select(domainClass, spec);
        interceptorHandler.doDelete(state);
        if (state.getCurrentType() == SpecInterceptor.SpecType.UPDATE) {
            return baseUpdate(state.getUpdateSpec());
        }

        CriteriaBuilder criteriaBuilder = entityManager.getCriteriaBuilder();
        CriteriaDelete<T> criteriaDelete = criteriaBuilder.createCriteriaDelete(domainClass);
        Root<T> root = criteriaDelete.from(domainClass);
        Predicate predicate = state.getSpec().toPredicate(root, null, criteriaBuilder);
        if (predicate != null) {
            criteriaDelete.where(predicate);
        }

        Query query = entityManager.createQuery(criteriaDelete);

        return query.executeUpdate();
    }

    public Spec<T> getIdSpec(ID id) {
        Spec<T> spec;
        if (objectId) {
            spec = helper -> {
                Iterator<String> iterator = entityInformation.getIdAttributeNames().iterator();
                while (iterator.hasNext()) {
                    String idName = iterator.next();
                    Object idVal = entityInformation.getCompositeIdAttributeValue(id, idName);
                    helper.equal(idName, idVal);
                }
                return helper.and().endAnd();
            };
        } else {
            spec = helper -> helper.equal(this.idName, id)
                    .and().endAnd();
        }
        return spec;
    }

    public Spec<T> getIdSpec(Collection<ID> ids) {
        Spec<T> spec;
        if (objectId) {
            spec = helper -> {
                Iterable<String> iterable = entityInformation.getIdAttributeNames();
                for (ID id : ids) {
                    for (String idName : iterable) {
                        Object idVal = entityInformation.getCompositeIdAttributeValue(id, idName);
                        helper.equal(idName, idVal);
                    }
                }

                return helper.or().endOr();
            };
        } else {
            spec = helper -> helper.in(this.idName, ids)
                    .and().endAnd();
        }
        return spec;
    }

}
